/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.server;

import java.io.File;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import jp.fores.midori.core.util.ClassUtil;
import jp.fores.midori.core.util.StringUtil;
import jp.fores.midori.server.config.MidoriConfigManager;
import jp.fores.midori.server.container.Container;
import jp.fores.midori.server.plugin.MethodNameAndArgsLength;
import jp.fores.midori.server.plugin.RPCPluginUtil;
import jp.fores.midori.server.plugin.RPCTargetMethodManager;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * RPCの対象のサービスのリスティング情報を管理するクラス。<br>
 * <i>(SingletonパターンのSingleton役)</i><br>
 * (このクラスの全てのメソッドはスレッドセーフです。)<br>
 */
public class RPCServiceListingManager {
	//==========================================================
	//定数

	/**
	 * 自分自身のクラスの唯一のインスタンス
	 */
	private static final RPCServiceListingManager singleton = new RPCServiceListingManager();

	/**
	 * ログ出力用
	 */
	private static final Log log = LogFactory.getLog(RPCServiceListingManager.class);


	//==========================================================
	//フィールド

	/**
	 * サービスクラスのリスティング情報を作成する際のロック用オブジェクト
	 */
	private final Object serviceClassListingLock = new Object();

	/**
	 * サービスクラスのリスティング情報の文字列
	 */
	//(一度作成したリスティング情報をキャッシュしておくため)
	private String serviceClassListingStr = null;

	/**
	 * サービスクラスのメソッドのリスティング情報をキャッシュするMap
	 */
	private final Map<String, String> methodListingCache = new HashMap<String, String>();


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//コンストラクタ

	/**
	 * コンストラクタです。<br>
	 * (Singletonクラスなので、privateにして外部からインスタンスを作成できないようにしています。)<br>
	 */
	private RPCServiceListingManager() {
		//特に何も行わない
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//クラスメソッド

	/**
	 * このクラスの唯一のインスタンスを返します。
	 *
	 * @return このクラスの唯一のインスタンス
	 */
	public static RPCServiceListingManager getInstance() {
		//フィールドの自分自身のクラスの唯一のインスタンスを返します
		return singleton;
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//通常のメソッド

	/**
	 * サービスクラスの一覧を出力します。<br>
	 *
	 * @return 作成した文字列
	 * @throws Exception 例外
	 */
	public String listServiceClasses() throws Exception {
		//サービスクラスのリスティング情報を作成する際のロック用オブジェクトに対して同期をとる
		synchronized (this.serviceClassListingLock) {
			//フィールドのサービスクラスのリスティング情報の文字列がまだ設定されていない場合
			//(サービスクラスのリスティング情報はシステム稼働中に変更されることはないので、今までに一度でも作成したことがあれば使いまわす)
			if (this.serviceClassListingStr == null) {
				//==========================================================
				//ルートパッケージ配下のクラスを検索
				//(実際のファイルシステムやjarファイルの中身を検索する)

				//クラス名を格納するSetを生成
				//(クラス名の辞書順にソートしたいので、HashSetではなく、TreeSetを使う)
				Set<String> classNameSet = new TreeSet<String>();

				//設定内容のルートパッケージを取得
				String rootPackage = MidoriConfigManager.getInstance().getConfig().rootPackage;

				//ルートパッケージがnullの場合
				if (rootPackage == null) {
					//nullのままだと扱いにくいので、代わりに空文字列を使用する
					rootPackage = "";
				}

				//検索用のベース名
				//(ルートパッケージの「.」を「/」に変換して、さらに末尾に「/」を追加する)
				String baseName = rootPackage.replace('.', '/') + '/';

				//現在のスレッドのクラスローダーを取得
				ClassLoader loader = Thread.currentThread().getContextClassLoader();

				//クラスローダーから検索用のベース名に対応する全てのリソースを検索
				Enumeration<URL> resources = loader.getResources(baseName);

				//リソースのURLのEnumerationに対してループをまわす
				while (resources.hasMoreElements()) {
					//現在のリソースのURLを取得
					URL url = resources.nextElement();

					//デバッグログが有効な場合
					if (log.isDebugEnabled()) {
						//デバッグログ出力
						log.debug("URL:" + url);
					}

					//URLのプロトコルを取得
					String protocol = url.getProtocol();

					//プロトコルが「file」の場合
					if ("file".equals(protocol)) {
						//URLに対応するファイルオブジェクトを取得
						//(このファイルオブジェクトは、ルートパッケージのディレクトリに相当する)
						File file = new File(url.getFile());

						//ファイルが存在し、かつディレクトリの場合
						if (file.exists() && file.isDirectory()) {
							//ファイルシステムから再帰的にクラスを検索する
							recursiveFindClass(file, rootPackage, classNameSet);
						}
					}
					//プロトコルが「jar」の場合
					else if ("jar".equals(protocol)) {
						//URLの入れ子になっている部分のパスを抽出する
						//(プロトコルが「jar」の場合は、URLの先頭部分が「jar:flie:」というような形式になっているため)
						String nestedUrlPath = new URL(url.getPath()).getPath();

						//jarファイルのパスを取得
						//(URLの入れ子になっている部分のパスから、「!」以降の文字列を切り捨てた部分のみを使用する)
						String jarFilePath = nestedUrlPath.substring(0, nestedUrlPath.lastIndexOf('!'));

						//jarファイルのパスからJarFileクラスのインスタンスを生成
						JarFile jarFile = new JarFile(jarFilePath);

						//Jarファイルからクラスを検索する
						findClassFromJarFle(jarFile, rootPackage, classNameSet);
					}
					//それ以外の場合
					else {
						//デバッグログが有効な場合
						if (log.isDebugEnabled()) {
							//デバッグログ出力
							log.debug("処理できないプロトコルなので無視します。:" + protocol);
						}
					}
				}


				//==========================================================
				//クラス名のSetからサービスクラスのものだけを抽出して、リスティング用の文字列を作成

				//作業用StringBuilderのインスタンスを生成
				StringBuilder sb = new StringBuilder();

				//クラス名のSetに対してループをまわす
				for (String className : classNameSet) {
					//クラス名からクラスオブジェクトを取得
					Class clazz = ClassUtil.forName(className);

					//取得したクラスオブジェクトがサービスクラスのものの場合
					if (RPCPluginUtil.isServiceClass(clazz)) {
						//シンプルなクラス名を取得
						String simpleClassName = clazz.getSimpleName();

						//クラス名の最初の文字を小文字にする
						simpleClassName = Character.toLowerCase(simpleClassName.charAt(0))
								+ simpleClassName.substring(1);

						//パッケージ名を取得
						String packageName = clazz.getPackage().getName();

						//パッケージ名から必要な部分のみを抽出
						//(先頭部分からはルートパッケージ名の分を、末尾の部分からは「service」の分を取り除く)
						//(7は「service」の文字数)
						String tmp = packageName.substring(rootPackage.length(), packageName.length() - 7);

						//呼び出しパスとして、抽出したパッケージ名の「.」を「/」に変換したものと、シンプルなクラス名を結合
						String callPath = tmp.replace(".", "/")
								+ simpleClassName;

						//呼び出しパスとクラス名を作業用StringBuilderに追加
						sb.append(callPath).append(" ").append(className);

						//次の要素との区切りのための改行を作業用StringBuilderに追加
						sb.append("\n");
					}
				}

				//作業用StringBuilderを文字列に変換して、フィールドのサービスクラスのリスティング情報の文字列に設定する
				this.serviceClassListingStr = sb.toString();
			}
		}

		//フィールドのサービスクラスのリスティング情報の文字列を返す
		return this.serviceClassListingStr;
	}

	/**
	 * 指定されたサービスクラスのメソッド一覧の情報を文字列化します。<br>
	 *
	 * @param className サービスクラスのクラス名
	 * @param container コンテナ
	 * @return 作成した文字列
	 * @throws Exception 例外
	 */
	public String listServiceMethods(String className, Container container)
			throws Exception {
		//サービスクラスのメソッドのリスティング情報をキャッシュするMapに対して同期をとる
		synchronized (this.methodListingCache) {
			//サービスクラスのメソッドのリスティング情報をキャッシュするMapからクラス名を引数のキーにして文字列を取得
			String str = this.methodListingCache.get(className);

			//キャッシュから取得できなかった場合
			if (str == null) {
				//コンテナから呼び出し対象のサービスクラスのインスタンスを取得
				Object service = RPCPluginUtil.getService(container, className);

				//サービスクラスのRPCの対象となるメソッド情報のMapを取得
				Map<MethodNameAndArgsLength, Method> rpcTargetMethodMap = RPCTargetMethodManager.getInstance().getRPCTargetMethodMap(service.getClass());

				//作業用StringBuilderのインスタンスを生成
				StringBuilder sb = new StringBuilder();

				//RPCの対象となるメソッド情報のMapの値に対してループをまわす
				for (Method method : rpcTargetMethodMap.values()) {
					//==========================================================
					//メソッドの情報のうち、必要な情報だけを見やすいように文字列化する
					//(Generic型の情報も含めたいので、Generic系のメソッドを使う)

					//戻り値のTypeオブジェクト
					//(そのまま追加するのではなく、見やすい形式に変換してから追加する)
					sb.append(ClassUtil.typeToString(method.getGenericReturnType()));

					//区切りのスペース
					sb.append(" ");

					//メソッド名本体
					sb.append(method.getName());


					//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
					//引数部分

					//メソッドの引数部分の開始のための「(」
					sb.append("(");

					//引数のTypeオブジェクトの配列を取得
					Type[] paramterTypes = method.getGenericParameterTypes();

					//引数のTypeオブジェクトの配列に対してループをまわす
					for (int i = 0; i < paramterTypes.length; i++) {
						//ループの最初の1回以外の場合
						if (i != 0) {
							//前の要素との区切りのための「,」
							sb.append(",");
						}

						//引数のTypeオブジェクト
						//(そのまま追加するのではなく、見やすい形式に変換してから追加する)
						sb.append(ClassUtil.typeToString(paramterTypes[i]));
					}


					//メソッドの引数部分の終了のための「)」
					sb.append(")");
					//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/

					//次のメソッドとの区切りのための改行
					sb.append("\n");
				}

				//作業用StringBuilderの内容を文字列に変換する
				str = sb.toString();

				//今後のために、作成した文字列をサービスクラスのメソッドのリスティング情報をキャッシュするMapに登録する
				this.methodListingCache.put(className, str);
			}

			//キャッシュから取得、または新たに生成した文字列を返す
			return str;
		}
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//内部処理用

	/**
	 * ファイルシステムから再帰的にクラスを検索します。<br>
	 *
	 * @param dir ディレクトリ
	 * @param packageName パッケージ名
	 * @param classNameSet クラス名を格納するSet
	 */
	private void recursiveFindClass(File dir, String packageName,
			Set<String> classNameSet) {
		//ディレクトリの配下のファイルに対してループをまわす
		for (File file : dir.listFiles()) {
			//ファイル名を取得
			String fileName = file.getName();

			//ディレクトリの場合
			if (file.isDirectory()) {
				//起点のディレクトリを変更し、さらに引数のパッケージ名に現在のファイル名の分も加えた状態で、このメソッドを再帰的に呼び出す
				recursiveFindClass(file, concatPackage(packageName, fileName), classNameSet);
			}
			//それ以外の場合
			//(ファイルの場合)
			else {
				//ファイル名の末尾が「.class」の場合
				if (fileName.endsWith(".class")) {
					//ファイル名から拡張子を取り除いた文字列を取得
					//(パッケージを取り除いた、単純なクラス名にあたる部分)
					String simpleClassName = FilenameUtils.removeExtension(fileName);

					//引数のパッケージ名に単純なクラス名を加えた文字列を、クラス名を格納するSetに追加する
					classNameSet.add(concatPackage(packageName, simpleClassName));
				}
			}
		}
	}

	/**
	 * Jarファイルからクラスを検索します。<br>
	 *
	 * @param jarFile JarFileクラスのインスタンス
	 * @param rootPackage ルートパッケージ
	 * @param classNameSet クラス名を格納するSet
	 * @throws Exception 例外
	 */
	private void findClassFromJarFle(JarFile jarFile, String rootPackage,
			Set<String> classNameSet) throws Exception {
		//JarファイルのエントリのEnumerationを取得
		Enumeration<JarEntry> entries = jarFile.entries();

		//JarファイルのエントリのEnumerationを読み終わるまでループをまわす
		while (entries.hasMoreElements()) {
			//現在のエントリを取得
			JarEntry entry = entries.nextElement();

			//エントリ名を取得
			String entryName = entry.getName();

			//エントリ名がルートパッケージで始まっており、かつ「.class」で終わっている場合
			//(検索対象のクラスファイルの場合)
			if (entryName.startsWith(rootPackage)
					&& entryName.endsWith(".class")) {
				//クラス名として、エントリ名から拡張子を取り除いた文字列を取得
				String className = FilenameUtils.removeExtension(entryName);

				//クラス名の区切り文字を「\」または「/」から「.」に置換する
				//(Jarのエントリのディレクトリ部分の区切り文字は「\」または「/」だが、パッケージの区切り文字は「.」のため)
				className = className.replace('\\', '.').replace('/', '.');

				//クラス名を格納するSetに追加する
				classNameSet.add(className);
			}
		}
	}

	/**
	 * ベースとなるパッケージ名の末尾に別の名前(パッケージ名やクラス名)を結合します。<br>
	 * ベースとなるパッケージ名がnullや空文字列の場合も適切に処理します。<br>
	 *
	 * @param basePackageName ベースとなるパッケージ名
	 * @param addName 末尾に追加する別の名前(パッケージ名やクラス名)
	 * @return 結果の文字列
	 */
	private String concatPackage(String basePackageName, String addName) {
		//ベースとなるパッケージ名がnullまたは空文字列の場合
		if (StringUtil.isBlank(basePackageName)) {
			//末尾に追加する別の名前だけを返す
			return addName;
		}
		//それ以外の場合
		else {
			//ベースとなるパッケージ名と末尾に追加する別の名前を「.」で結合して返す
			return basePackageName + "." + addName;
		}
	}
}
